package hydrap2p.proxy.manager;

import hydrap2p.ServiceLocation;
import hydrap2p.logging.Level;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.TreeMap;

import org.apache.thrift.TException;

/**
 * Forward connections from a certain listening port to a certain listener port on the other side of the connection
 * @author Hufman
 *
 */
public class ServiceTunnel implements Runnable {
	private static final short MaxEphemeralTries=50;
	
	private boolean running;
	private int port;
	private ServerSocket listenSocket;
	
	private int peerid;
	private ServiceLocation remoteport;
	
	private Map<Integer, Connection> activeconnections;		// keyed on the client's source port
	
	public ServiceTunnel(int peerid, ServiceLocation remoteport) throws IOException
	{
		int tries=MaxEphemeralTries;
		while (tries>0 && listenSocket==null)
		{
			try {
				listenSocket=new ServerSocket(0);	// ephemeral port
			} catch (IOException e) {
				// Could not create socket
				tries--;
			}
		}
		if (listenSocket!=null && listenSocket.getLocalPort()>-1)
			port=listenSocket.getLocalPort();
		else
			throw new IOException("Could not bind to port");
		
		activeconnections=new TreeMap<Integer, Connection>();
		this.peerid=peerid;
		this.remoteport=remoteport;
	}
	
	@Override
	public void run() {
		running=true;
		while (running)
		{
			try {
				Socket accepted=listenSocket.accept();
				//ProxyLogging.log(Level.DEBUG, "ProxyManager - ServiceTunnel", "Accepting connection from "+accepted.getInetAddress()+":"+accepted.getPort()+" to "+listenSocket.getLocalPort());
				Connection temp=new Connection(accepted);
				new Thread(temp, temp.toString()).start();
				activeconnections.put(accepted.getPort(), temp);
				
			} catch (IOException e) {
				// Socket was disconnected
				running=false;
			}
		}
		ServiceTunnels.getInstance().removeTunnel(port);
	}
	
	public void shutdown() {
		running=false;
		try {
			listenSocket.close();
		} catch (IOException e) {
			// Shutting down
		}
	}
	
	private void deleteConnection(int port)
	{
		activeconnections.remove(port);
	}
	
	public int getPort()
	{
		return port;
	}
	public int getPeerID()
	{
		return peerid;
	}
	public ServiceLocation getRemotePort()
	{
		return remoteport;
	}
	
	public void returnData(ServiceLocation client, byte[] data)
	{
		Connection connection;
		synchronized(activeconnections)
		{
			connection=activeconnections.get(client.port);
		}
		if (connection!=null)
		{
			connection.returnData(data);
		}
	}

	public void closeDataConnection(int sourceport) {
		Connection connection;
		synchronized(activeconnections)
		{
			connection=activeconnections.remove(sourceport);
		}
		if (connection!=null)
		{
			connection.closeRunnerConnection();
		}
	}
	
	@Override
	public String toString()
	{
		return "ServiceTunnel: "+port+" -> "+peerid+":"+remoteport.port;
	}
	
	private class Connection implements Runnable
	{
		private Socket connection;
		private int port;
		private ServiceLocation from;
		public Connection(Socket connection)
		{
			this.connection=connection;
			port=connection.getPort();
			from=new ServiceLocation(connection.getInetAddress().getHostAddress(), connection.getPort());
		}
		
		public void returnData(byte[] data) {
			try {
				//ProxyLogging.log(Level.DEBUG, "ProxyManager - ServiceTunnel", "Sending returned data to "+from);

				synchronized(connection.getOutputStream())
				{
					connection.getOutputStream().write(data);
				}
			} catch (IOException e) {
				// Could not communicate
				activeconnections.remove(port);
				try {
					connection.close();
				} catch (IOException e1) {
					// Give up
				}
			}
		}

		public void closeRunnerConnection() {
			try {
				connection.close();
			} catch (IOException e1) {
				// Give up
			}
		}

		@Override
		public void run()
		{
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - ServiceTunnel", "Starting new servicetunnel client processor");
			byte[] temp=new byte[16*1024];
			int size=0;
			while (!connection.isClosed() && size>=0)
			{
				try {
					size=connection.getInputStream().read(temp);
				} catch (IOException e) {
					// disconnected
					activeconnections.remove(port);
					try {
						ProxyLogging.log(Level.DEBUG, "ProxyManager - ServiceTunnel", "Rudely closed connection from "+from+" to "+remoteport);
						connection.close();
					} catch (IOException e1) {
						// Give up
						break;
					}
				}
				
				if (size>=0)
				{
					try {
						//ProxyLogging.log(Level.DEBUG, "ProxyManager - ServiceTunnel", "Sending data from "+from+" to "+remoteport);

						ManagerConnectionManager.getInstance().sendData(peerid, from, remoteport, temp, size);
					} catch (TException e) {
						// Failure to communicate with peer, somehow
						activeconnections.remove(port);
						try {
							connection.close();
						} catch (IOException e1) {
							// Give up
							break;
						}
					}
				}
			}
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - ServiceTunnel", "Closing connection from "+from+" to "+remoteport);
			closeDataConnection(port);
		}
		
		@Override
		public String toString()
		{
			return "ServiceTunnel.Connection: "+from.address+":"+from.port+" -> "+peerid+":"+remoteport;
		}
	}

}
